---
title: 02 回显
---

import PlusIcon from '@material-ui/icons/AddSharp'
import FlagIcon from '@material-ui/icons/FlagSharp'
import PlayIcon from '@material-ui/icons/PlayArrowSharp'
import RestartIcon from '@material-ui/icons/ReplaySharp'
import SaveIcon from '@material-ui/icons/SaveSharp'
import StopIcon from '@material-ui/icons/StopSharp'

你已经成功运行了一个简单的 HTTP 服务器来打招呼。在本单元里，你将学习如何让服务器响应动态内容。

## 使用集成开发环境（IDE）

从本章开始，随着新功能不断增加，我们的脚本也将越变越复杂。很快，我们的代码就会增长到无法容纳在一行里的程度。为了更好的维护性和可读性，我们最好开始使用 _文件_ 来组织我们的脚本。

你可以用你喜爱的任何文本编辑器来编写脚本。在本教程中，我们将使用 Pipy 内置的集成开发环境（IDE）。它提供了自动补全和 [Pipy API](/reference/api) 文档，而且，在你键入代码时，还会可视化显示相应的管道布局。

### Pipy 仓库

当你使用内置 IDE 时，正在编辑的文件会被存储在一个叫做 _"Pipy 仓库"_ 的虚拟文件系统里面。存储在这个仓库里面的脚本能够被共享给远程的 Pipy 实例。你可以通过这种方式部署一个 Pipy _工作者_ 集群，并且从仓库端远程控制它们。

如果不是用作远程集群控制器，Pipy 仓库还可以启动本地的 Pipy 工作者用于开发和调试。这也是本教程所关注的重点。

> 默认情况下，Pipy 仓库仅把文件保存在内存中，当 Pipy 退出时，文件也就随之丢失了。也可以让 Pipy 把文件保存在一个本地数据库里面。关于这方面的更多信息，请参阅 [Pipy Repo](https://flomesh.io/docs/en/operating/repo)。

首先，我们以 _仓库模式_ 启动 Pipy。方法是，不带任何命令行参数来运行 `pipy`：

``` sh
pipy
```

Pipy 启动并运行起来以后，你会在终端上看到如下信息：

```
[INF] [admin] Starting admin service...
[INF] [listener] Listening on TCP port 6060 at ::

=============================================

  You can now view Pipy GUI in the browser:

    http://localhost:6060/

=============================================
```

现在，打开你最喜爱的 Web 浏览器，指向 [http://localhost:6060](http://localhost:6060)。你将看到内置的 _Pipy 管理界面_。

### 创建一个 Pipy 代码库

现在让我们在仓库中重写上次那个 _"Hello World"_ 脚本。

1. 点击 **New Codebase**。在弹出的窗口中，输入 `/hello` 作为 _Codebase name_，然后点击 **Create**，界面会跳转到代码编辑器，以编辑这个代码库。

2. 点击上方的 <PlusIcon/> 来添加一个新文件，输入 `/hello.js` 做为文件名，然后点击 **Create**。

3. 在左侧的 _文件列表_ 中选择新建的文件，点击 <FlagIcon/> 使该文件成为代码库的入口。意思是，下次运行程序时，将从求解这个文件开始。

4. 输入 **hello.js** 的代码：

``` js
pipy()

  .listen(8080)
  .serveHTTP(new Message('Hi, there!\n'))
```

5. 点击 <SaveIcon/> 以保存文件。

### 启动程序

点击 <PlayIcon/> 来运行代码。此时，_控制台窗口_ 将弹出，并显示跟上次终端里一样的输出。

```
[INF] [config]
[INF] [config] Module 
[INF] [config] =======
[INF] [config]
[INF] [config]  [Listen on 8080 at 0.0.0.0]
[INF] [config]  ----->|
[INF] [config]        |
[INF] [config]       serveHTTP -->|
[INF] [config]                    |
[INF] [config]  <-----------------|
[INF] [config]  
[INF] [listener] Listening on TCP port 8080 at 0.0.0.0
```

做跟上次一样的测试，你会看到同样的结果：

``` sh
$ curl localhost:8080
Hi, there!
```

## 回显服务器

现在，我们修改一下脚本，多开一个端口来复述你发送的消息：

``` js
  pipy()

    .listen(8080)
    .serveHTTP(new Message('Hi, there!\n'))

+   .listen(8081)
+   .serveHTTP(msg => new Message(msg.body))
```

正如你所见，我们并没有创建一个新的代码库。相反，我们仅仅拓展了上次的代码，多加了一个 _端口管道布局_。

这个新的管道布局监听 8081 端口，包含了同样的单个 [serveHTTP()](/reference/api/Configuration/serveHTTP) 过滤器。但是与上次不同，这次我们提供给 _serveHTTP()_ 的构造参数不是一个 [Message](/reference/api/Message) 对象，而是一个 **函数**。

这个 **函数** 就是我们实现动态服务器的秘密配方。

### 代码剖析

在第一个管道布局中，`new Message()` 仅在 Pipy 启动阶段的 _配置时_ 求解一次，无论 Pipy 在 _运行时_ 接收到多少请求，我们都只有唯一一个 _Message_ 对象，因此，来自 Pipy 的响应永远不会发生改变。

在第二个管道布局里，`msg => new Message()` 同样仅在 _配置时_ 求解一次，但不同于第一种情况，这次求解得到的是一个 **函数**，而不是一个 _Message_ 对象。在运行时，对于每个请求，这个函数都会求解一次以得到一个响应消息，这个消息每次都可以不一样。

这个函数有个输入变量叫 `msg`，该变量就是接收到的 HTTP 请求，包裹在一个 [Message](/reference/api/Message) 对象里面。该函数应当输出一个 _Message_ 作为响应。这里我们只是用请求消息体的内容简单地拼凑出一个新 _Message_，对于我们这个简单的 "回显服务器" 而言足够了。

### 测试

别忘记在测试之前保存你的修改，如果 Pipy 还在运行着旧版本的代码，你可以：

1. 要么点击 <StopIcon/> 以停止旧版本，然后点击 <PlayIcon/> 来启动新版本。

2. 要么点击 <RestartIcon/> 重启程序，一次性切换到新版本。

还是老样子，我们用 `curl` 做个测试：

``` sh
curl localhost:8081 -d 'hello'
```

你会看到：

```
hello
```

漂亮！

## 更多回显

除了 “鹦鹉学舌”，我们当然还能做更多事情。现在，让我们不仅仅以客户端所说的东西进行响应，我们还给出 TA 的位置，好不好？

``` js
  pipy()

    .listen(8080)
    .serveHTTP(new Message('Hi, there!\n'))

    .listen(8081)
    .serveHTTP(msg => new Message(msg.body))

+   .listen(8082)
+   .serveHTTP(
+     msg => new Message(
+       `You are requesting ${msg.head.path} from ${__inbound.remoteAddress}\n`
+     )
+   )
```

同样的，我们在已有的东西上添加。这次，用一个新的管道布局监听 8082 端口，使用相同的 _serveHTTP()_ 过滤器，但却是不同的函数。

### 代码剖析

这次我们将返回动态生成的文本，我们使用了 JavaScript 的 [_模板字符串_](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Template_literals)。在模板字符串里，我们有两个可变部分。

* `msg.head.path` 是 HTTP 请求里的 URI
* `__inbound.remoteAddress` 是客户端的 IP 地址

`__inbound` 变量是一个内置的 _上下文变量_，包含当前入站连接的地址/端口信息。我们称它为 _上下文变量_ 是因为它的值依赖于上下文。在处理多个并发入站连接的多个管道中，它的值各不相同，因为每个管道都工作在一个不同的 _上下文_ 里面。

> 对于 _上下文变量_ 的深度解释，请参阅 _概念_ 里的 [上下文](/intro/concepts#上下文)。

### 测试

现在如果你在终端上输入：

``` sh
curl localhost:8082/hello
```

你将得到：

```
You are requesting /hello from 127.0.0.1
```

## 总结

在这部分教程里，你学会了如何针对客户端请求生成动态响应内容，同时也初步体验了一下 Pipy 里的 _上下文变量_ 是如何工作的。

### 要点

1. 过滤器参数仅在 _配置时_ 求解一次，所以在 _运行时_ 其值是静态的。想要动态值的话，参数必须是能够输出 “动态” 值的 “静态的” **函数**。

2. _上下文变量_ 在并发管道之间拥有相互隔离的状态。内置的上下文变量之一是 `__inbound`，它包含了当前入站连接的地址/端口信息。

### 接下来

我们已经看到如何利用 Pipy 快速搭建一个临时服务器，但是这些都不是 Pipy 的主要用途。我们使用 Pipy 主要还是作为网络代理，这就是接下来我们要探究的事情。
